<?php

namespace app\model\common;

use think\Model;
use think\model\Collection;

/**
 * 基础模型
 */
abstract class BaseModel extends Model
{
	/**
	 * ---- 属性配置 ----
	 */
	protected $json = [];
	protected $jsonAssoc = true;

	protected $type = [];

	protected $hidden = [];

	/**
	 * 自动处理分页参数
	 * 规定：当前页码:_page、每页条数:_limit、排序字段:_sort_key、排序类型:_sort_type
	 * 注意：当和join一起使用时，请先设置别名，如  Model::alias('a')->pagesort()->join(...)->select()
	 * 用法： Model::pagesort()->where(xxx)->select() 或 $model->pagesort()->select();
	 * @param array $ignoreSortFields 要忽略排序的字段
	 * @param bool|string $sortByPk 是否默认增加按主键排序规则，只能是false或'ASC'/'DESC'
	 */
	public function scopePagesort(\think\db\Query $query, $ignoreSortFields = [], $sortByPk = 'DESC'): \think\db\Query
	{
		// $alias 会被设为空字符串或  alias. 可直接拼接到后续语句中
		$request = request();

		$sort_key = $request->param('_sort_key');
		$limit = $request->param('_limit/d');
		$index = $request->param('_page/d');

		if ($limit) {
			$index = $index ? intval($index) : 1;
			$query->page($index, intval($limit));
		}

		$table = $query->getTable();
		$options = $query->getOptions();
		$pk = $query->getPk();
		$alias = '';
		if (isset($options['alias']) && is_array($options['alias'])) {
			foreach ($options['alias'] as $k => $v) {
				if ($k == $table) {
					$alias = $v . '.';
					break;
				}
			}
		};

		if ($sort_key) {
			$sort_type = strtoupper($request->param('_sort_type', 'DESC'));
			if (!in_array($sort_type, ['ASC', 'DESC'])) {
				$sort_type = 'DESC';
			}

			$fields = $query->getTableFields();
			if (in_array($sort_key, $fields) && !in_array($sort_key, $ignoreSortFields)) {
				// 原始字段排序
				if ($sortByPk) {
					$query->order([
						$alias . $sort_key => $sort_type,
						$alias . $pk => $sortByPk
					]);
				} else {
					$query->order($alias . $sort_key, $sort_type);
				}
			}
		} else {
			if ($sortByPk) {
				$query->order($alias . $pk, $sortByPk);
			}
		}
		return $query;
	}


	/**
	 * json数组字段 查询构造器
	 *
	 * @param [type] $query query类
	 * @param String $key 字段名
	 * @param [type] $value 查询的值，例如 1，[1,2]，["id"=>1]，["id"=>"1","name"=>"hhh"]，["user"=>["age"=>18]]
	 * @param string $where 查询方式（AND或OR）
	 * @return void
	 */
	public function scopeQueryJsonArray($query, String $key, $value, String $where = 'AND')
	{
		if (is_array($value)) {
			//是纯索引数组用JSON_ARRAY，否则用JSON_OBJECT
			if (arrayIsPureNumber($value)) {
				$value = array_map(fn($v) => is_numeric($v) ? $v : "'$v'", $value);
				$value_result = implode(',', $value);
				$json_fun = 'JSON_ARRAY(' . $value_result . ')';
			} else {
				//递归生成json object格式
				function jsonObjcet($value)
				{
					$result = [];
					foreach ($value as $k => $v) {
						$list = '';
						$list .= "'$k',";
						$v_result = $v;
						if (is_array($v)) {
							$v_result =  jsonObjcet($v);
						}
						if (!is_int($v_result) && !str_starts_with($v_result, 'JSON_OBJECT')) $v_result = "'$v_result'";
						$list .= $v_result;
						$result[] = $list;
					}
					return 'JSON_OBJECT(' . implode(',', $result) . ')';
				}

				$json_fun = jsonObjcet($value);
			}
		} else {
			$value = is_numeric($value) ? $value : "'$value'";
			$json_fun = 'JSON_ARRAY(' . $value . ')';
		}

		$query->whereRaw('JSON_CONTAINS(' . $key . ',' . $json_fun  . ')', [], $where);
	}

	/** 
	 * 自动字段筛选
	 * 根据模型的 allowFields() 获取字段，具体逻辑参见该方法
	 * 根据 allowFields 从 Request 中获取对应同名参数，参数支持 数组、非数组
	 * 数组：用whereIn查询，其中空数组会被忽略
	 * 非数组：直接用where查询
	 * @param array $ignoreFields 要忽略的字段
	 */
	public function scopeFilterFields($query, $ignoreFields = [])
	{
		$request = request();
		$param = $request->only(array_diff($this->checkAllowFields(), $ignoreFields));
		foreach ($param as $key => $val) {
			is_array($val)
				? $query->when(!empty($val), fn($q) => $q->whereIn($key, $val))
				: $query->where($key, $val);
		}
		return $query;
	}

	/**
	 * 自动设置字段with\append\hidden
	 * 前端提交 _with _append _hidden字段
	 * @param array $ignoreFields 要忽略的字段
	 */
	public function scopeWithAppends($query, $ignoreFields = [])
	{
		$request = request();
		$with = $request->param('_with');
		if ($with && is_array($with) && count($with)) {
			$query->with(array_diff($with, $ignoreFields));
		}
		// append和hidden都支持第二个参数 $merge，表示是否合并到已有的append或hidden中
		$append = $request->param('_append');
		if ($append && is_array($append) && count($append)) {
			$query->append(array_diff($append, $ignoreFields), true);
		}
		$hidden = $request->param('_hidden');
		if ($hidden && is_array($hidden) && count($hidden)) {
			$query->hidden(array_diff($hidden, $ignoreFields), true);
		}
		return $query;
	}

	/**
	 * 自动筛选时间字段
	 * 前端提交 _time_filter 字段，类型是 FilterType，可以是单个或数组
	 * FilterType: object
	 * 		field {string} 字段名
	 * 		start {string} 筛选起始时间
	 * 		end {string} 筛选结束时间
	 * @param array $ignoreFields 要忽略的字段
	 */
	public function scopeFilterTime($query, $ignoreFields = [])
	{
		$request = request();
		$filters = $request->param('_time_filter');

		$isSingleFilter = $filters[0]['field'] ?? false;
		if (!$isSingleFilter) $filters = [$filters];

		foreach ($filters as $filter) {
			if (!isset($filter['field']) || !$filter['field'] || in_array($filter['field'], $ignoreFields)) continue;
			$query->when(isset($filter['start']) && $filter['start'], fn($q) => $q->whereTime($filter['field'], '>=', $filter['start']));
			$query->when(isset($filter['end']) && $filter['end'], fn($q) => $q->whereTime($filter['field'], '<=', $filter['end']));
		}
	}

	/**
	 * 原生查询替换has()功能
	 * 使用场景：原生的has()方法会干扰后续的 withSum 等，可以用这个方法替代
	 * 例子: 
	 * 		查询至少有一条关联 input_log 记录
	 * 			GameLog::scope('FilterHas', 'input_log', 'game_id')
	 * 		查询更复杂条件
	 * 			GameLog::scope('FilterHas', 'input_log', 'game_id', function($subQuery) {
	 * 				return $subQuery->where('amount', '>', 100); // 存在至少有一条投入大于100的记录
	 * 				return $subQuery->group('game_id')->having('COUNT(*)>10'); // 存在10条以上记录的
	 * 			});
	 * @param Query $query
	 * @param string $table 关联表名
	 * @param string $foreignKey 关联表的外键
	 * @param string $subQueryFunction 闭包对子查询额外处理
	 * @return Query
	 */
	public function scopeFilterHas($query, string $table, string $foreignKey, $subQueryFunction = null)
	{
		// 使用whereExists实现
		return $query->whereExists(function ($subQuery) use ($query, $table, $foreignKey, $subQueryFunction) {
			$mainTableName = $query->getTable();
			$mainPk = $query->getPk();
			$subQuery->table($table)->whereColumn($table . '.' . $foreignKey, '=', $mainTableName . '.' . $mainPk);
			if ($subQueryFunction instanceof \Closure) {
				$subQuery = $subQueryFunction($subQuery);
			}
		});
	}

	/**
	 * 经纬度距离计算
	 * @param [type] $query
	 * @param array $target [经度, 纬度]
	 * @param string $sort 排序方式 ASC近到远 DESC远到近
	 * @param null|string $appendField 是否将距离追加到查询结果中，会通过 field('*, $appendField')实现
	 * @param string $lngField 经度字段
	 * @param string $latField 纬度字段
	 */
	public function scopeDistance($query, $target = [0, 0], $sort = 'ASC', $appendField = null, $lngField = 'pos_lng', $latField = 'pos_lat')
	{
		// 计算距离(米)
		$distanceFormula = sprintf(
			'1000 * 6371 * ACOS(COS(RADIANS(%s)) * COS(RADIANS(%s)) * COS(RADIANS(%s) - RADIANS(%s)) + SIN(RADIANS(%s)) * SIN(RADIANS(%s)))',
			$target[1],
			$latField,
			$target[0],
			$lngField,
			$target[1],
			$latField
		);

		$query->orderRaw($distanceFormula . ' ' . $sort)
			->when(!is_null($appendField), fn($q) => $q->field('*')->field($distanceFormula . ' AS ' . $appendField));
	}

	/**
	 * 通过某个数组ID字段获取多个关联模型数据
	 * @example $user['tags'] = $user->getRelationArrayManyData(UserTag::class, $user['tag_ids'], 'id')->visible(['name']);
	 * @param string|class $relationModelName 关联模型
	 * @param array $ids ID数组
	 * @param string $localKey 关联模型的主键
	 */
	public function getRelationArrayManyData($relationModelName, $ids, $localKey = 'id')
	{
		$results = new Collection();
		if (!is_array($ids)) return $results;

		$modelClass = class_exists($relationModelName) ? $relationModelName : ('\\app\\model\\' . $relationModelName);

		$data = $modelClass::whereIn($localKey, $ids)->select();
		foreach ($ids as $id) {
			foreach ($data as $d) {
				if ($d[$localKey] == $id) {
					$results->push($d);
					break;
				}
			}
		}
		return $results;
	}

	/**
	 * 创建模型内唯一code
	 * 方法内部会调用 static::find() 查询该code是否存在，如果存在会换一个
	 * @param integer $len 编码长度（除前缀外）
	 * @param string $prefix code前缀
	 * @param string $field 要检查重复的字段
	 * @return string
	 */
	public static function createModelUniqueCode($len = 10, $prefix = '', $field = 'code', $charList = null): string
	{
		$code = $prefix . createRandomString($len);
		while (static::where($field, $code)->find()) {
			$code = $prefix . createRandomString($len, $charList);
		}
		return $code;
	}
}
